#!/bin/sh
# shellcheck disable=SC3043 disable=SC2086 disable=SC2059 disable=SC2039 disable=SC2034

underline="________________________________________________________________"

restore_cursor() {
  tput cnorm
}

cleanup() {
  exit_code=$1
  stty echo 1>/dev/null 2>&1
  echo
  if [ -n "$allpids" ]; then
    for pid in $allpids; do
      kill "$pid" 1>/dev/null 2>&1
    done
    tput sgr0
  fi
  restore_cursor

  if [ "$exit_code" -ne 0 ]; then
    echo
  fi

  exit "$exit_code"
}
# 这会尝试捕获任何退出，无论是正常退出还是强制退出（例如 Ctrl-C）
if [ "${INTERACTIVE_MODE}" != "off" ]; then
  trap 'cleanup 2' INT
  trap 'cleanup 3' QUIT
  trap 'cleanup 15' TERM
  trap 'cleanup 0' EXIT
fi

#一些颜色和格式常量
if which 'tput' >/dev/null; then
  if [ -t 1 ]; then
    ncolors=$(tput colors)
    if [ "$ncolors" -ge 8 ]; then
      RED=$(tput setaf 1)       #字体红色 31
      GREEN=$(tput setaf 2)     #字体绿色 32
      YELLOW=$(tput setaf 3)    #字体黄色 33
      BLUE=$(tput setaf 4)      #字体蓝色 34
      MAGENTA=$(tput setaf 5)   #字体紫色 35
      CYAN=$(tput setaf 6)      #字体青色 36
      WHITE=$(tput setaf 7)     #字体白色 37
      REDBG=$(tput setab 1)     #背景红色 41
      GREENBG=$(tput setab 2)   #背景绿色 42
      YELLOWBG=$(tput setab 3)  #背景黄色 43
      BLUEBG=$(tput setab 4)    #背景蓝色 44
      MAGENTABG=$(tput setab 5) #背景紫色 45
      CYANBG=$(tput setab 6)    #背景青色 46
      WHITEBG=$(tput setab 7)   #背景白色 47

      BOLD=$(tput bold)      #设置粗体 1
      DIM=$(tput dim)        #打开高亮模式
      REV=$(tput rev)        #突出模式，所有输出字符都突出显示
      UNDERLINE=$(tput smul) # 4设置下划线(模拟彩色显示器的颜色)许多终端不支持此功能
      NORMAL=$(tput sgr0)    #重新设置属性到缺省设置 0
    fi
  fi
else
  echo "未找到tput,已禁用彩色输出."
  RED=''
  GREEN=''
  YELLOW=''
  BLUE=''
  MAGENTA=''
  CYAN=''
  WHITE=''
  REDBG=''
  GREENBG=''
  YELLOWBG=''
  BLUEBG=''
  MAGENTABG=''
  CYANBG=''

  BOLD=''
  UNDERLINE=''
  NORMAL=''
fi

# slog - 日志库
# LOG_PATH - 在脚本中定义 $LOG_PATH 以记录到文件，否则
# 只写到标准输出.

# LOG_LEVEL_STDOUT - 定义以确定高于哪个级别进入标准输出.
# 默认情况下，所有日志级别都会写入标准输出.
LOG_LEVEL_STDOUT="INFO"

# LOG_LEVEL_LOG - 定义以确定哪个级别进入 LOG_PATH.
# 默认情况下，所有日志级别都会写入 LOG_PATH.
LOG_LEVEL_LOG="INFO"

# 用户可能希望引用的有用全局变量
SCRIPT_ARGS="$*"
SCRIPT_NAME="$0"
SCRIPT_NAME="${SCRIPT_NAME#\./}"
SCRIPT_NAME="${SCRIPT_NAME##/*/}"

#--------------------------------------------------------------------------------------------------
# 开始日志记录部分
if [ "${INTERACTIVE_MODE}" = "off" ]; then
  # 然后我们不关心日志颜色
  LOG_DEFAULT_COLOR=""
  LOG_ERROR_COLOR=""
  LOG_INFO_COLOR=""
  LOG_SUCCESS_COLOR=""
  LOG_WARN_COLOR=""
  LOG_DEBUG_COLOR=""
else
  LOG_DEFAULT_COLOR=$(tput sgr0)
  LOG_ERROR_COLOR=$(tput setaf 1)
  LOG_INFO_COLOR=$(tput setaf 6)
  LOG_SUCCESS_COLOR=$(tput setaf 2)
  LOG_WARN_COLOR=$(tput setaf 3)
  LOG_DEBUG_COLOR=$(tput setaf 4)
fi

# 此函数清除彩色输出中使用的任何控制字符的输出
# 它被设计为通过管道传输需要清理的文本。 擦洗过的
# 文字会从另一边出来！
prepare_log_for_nonterminal() {
  # 本质上，这会去除日志颜色的所有控制字符
  sed -E 's/\x1B\[[0-9;]*[mK]//g; s/\x1B\([A-Za-z]//g' | tr -d '[:cntrl:]'
}

log_date() {
  local log_date_level="$1"
  echo "[$(date +"%Y-%m-%d %H:%M:%S %Z")] [$log_date_level] "
}

log() {
  local log_text="$1"
  local log_level="$2"
  local log_color="$3"
  local log_set="$4"

  # 用于与 LOG_LEVEL_STDOUT 和 LOG_LEVEL_LOG 进行比较的级别
  local LOG_LEVEL_DEBUG=0
  local LOG_LEVEL_INFO=1
  local LOG_LEVEL_SUCCESS=2
  local LOG_LEVEL_WARNING=3
  local LOG_LEVEL_ERROR=4

  # 默认级别为“信息”
  [ -z "${log_level}" ] && log_level="INFO"
  [ -z "${log_color}" ] && log_color="${LOG_INFO_COLOR}"

  # 验证 LOG_LEVEL_STDOUT 和 LOG_LEVEL_LOG 因为它们将被评估。
  case $LOG_LEVEL_STDOUT in
  DEBUG | INFO | SUCCESS | WARNING | ERROR) ;;
  *)
    LOG_LEVEL_STDOUT=INFO
    ;;
  esac
  case $LOG_LEVEL_LOG in
  DEBUG | INFO | SUCCESS | WARNING | ERROR) ;;
  *)
    LOG_LEVEL_LOG=INFO
    ;;
  esac

  log_lang() {
    if [ "${log_level}" = DEBUG ]; then
      log_level=调试
    elif [ "${log_level}" = INFO ]; then
      log_level=信息
    elif [ "${log_level}" = SUCCESS ]; then
      log_level=成功
    elif [ "${log_level}" = WARNING ]; then
      log_level=警告
    elif [ "${log_level}" = ERROR ]; then
      log_level=错误
    fi
  }

  # 检查 LOG_LEVEL_STDOUT 以查看该级别的条目是否进入 STDOUT。
  # XXX 当您的语言没有哈希数据结构时，就会发生这种可怕的情况。
  eval log_level_int="\$LOG_LEVEL_${log_level}"
  eval log_level_stdout="\$LOG_LEVEL_${LOG_LEVEL_STDOUT}"
  # shellcheck disable=SC2154
  if [ "$log_level_stdout" -le "$log_level_int" ]; then
    # STDOUT
    log_lang
    printf "%s[%s]%s %s\\n" "$log_color" "$log_level" "$LOG_DEFAULT_COLOR" "$log_text"
  fi
  # 这一切都非常棘手； 计算出要比较的数值。
  eval log_level_log="\$LOG_LEVEL_${LOG_LEVEL_LOG}"
  # 检查 LOG_LEVEL_LOG 以查看此级别的条目是否进入 LOG_PATH
  # shellcheck disable=SC2154
  if [ "$log_level_log" -le "$log_level_int" ]; then
    # LOG_PATH 减去花式裤子的颜色
    if [ -n "$LOG_PATH" ]; then
      log_lang
      today=$(date +"%Y-%m-%d %H:%M:%S %Z")
      printf "[%s] [%s] %s\\n" "$today" "$log_level" "$log_text" >>"$LOG_PATH"
    fi
  fi

  return 0
}

log_info() { log "$@"; }
log_success() { log "$1" "SUCCESS" "${LOG_SUCCESS_COLOR}"; }
log_error() { log "$1" "ERROR" "${LOG_ERROR_COLOR}"; }
log_warning() { log "$1" "WARNING" "${LOG_WARN_COLOR}"; }
log_debug() { log "$1" "DEBUG" "${LOG_DEBUG_COLOR}"; }

log_success_set() { log "$1" "SUCCESS" "${LOG_SUCCESS_COLOR}" "set"; }
log_error_set() { log "$1" "ERROR" "${LOG_ERROR_COLOR}" "set"; }
log_warning_set() { log "$1" "WARNING" "${LOG_WARN_COLOR}" "set"; }
log_debug_set() { log "$1" "DEBUG" "${LOG_DEBUG_COLOR}" "set"; }

# 结束记录部分
#--------------------------------------------------------------------------------------------------

# spinner - 记录以在发生长时间运行的任务时提供微调器

# 配置变量，在采购后设置这些变量以更改行为。
SPINNER_COLORNUM=2                # 什么颜色？ 如果 COLORCYCLE=1，则无关。
SPINNER_COLORCYCLE=1              # 颜色循环吗？
SPINNER_DONEFILE="stopspinning"   # 要退出的文件的路径/名称。
SPINNER_SYMBOLS="WIDE_ASCII_PROG" # 包含符号的变量的名称。
SPINNER_CLEAR=1                   # 完成后将行清空。

spinner() {
  # 添加此陷阱以确保在脚本完成或被杀死时，
  # 旋转器终止并且光标恢复。
  trap 'restore_cursor; exit' INT QUIT TERM EXIT
  # 最安全的选择是其中之一。 根本不需要 Unicode。
  local WIDE_ASCII_PROG="[>-] [->] [--] [--]"
  local WIDE_UNI_GREYSCALE2="▒▒▒ █▒▒ ██▒ ███ ▒██ ▒▒█ ▒▒▒"

  local SPINNER_NORMAL
  SPINNER_NORMAL=$(tput sgr0)

  eval SYMBOLS=\$${SPINNER_SYMBOLS}

  # 获取父进程 PID
  SPINNER_PPID=$(ps -p "$$" -o ppid=)
  while :; do
    tput civis
    for c in ${SYMBOLS}; do
      if [ $SPINNER_COLORCYCLE -eq 1 ]; then
        if [ $SPINNER_COLORNUM -eq 7 ]; then
          SPINNER_COLORNUM=1
        else
          SPINNER_COLORNUM=$((SPINNER_COLORNUM + 1))
        fi
      fi
      local SPINNER_COLOR
      SPINNER_COLOR=$(tput setaf ${SPINNER_COLORNUM})
      tput sc
      env printf "${SPINNER_COLOR}${c}${SPINNER_NORMAL}"
      tput rc
      if [ -f "${SPINNER_DONEFILE}" ]; then
        if [ ${SPINNER_CLEAR} -eq 1 ]; then
          tput el
        fi
        rm -f ${SPINNER_DONEFILE}
        break 2
      fi
      # 这是有疑问的。 带小数秒的 sleep 并不总是可用，
      # 但似乎不会破坏东西（当没有时）。
      env sleep .2
      # 检查以确保家长仍在继续； 处理叹气/杀死
      if [ -n "$SPINNER_PPID" ]; then
        # 这是荒谬的。 ps 在 ppid 调用中添加一个空格，这会中断
        # 此 ps 出现“垃圾选项”错误。
        # XXX 如果 ps 产生奇怪的输出，则可能会出现问题。
        # shellcheck disable=SC2086
        SPINNER_PARENTUP=$(ps --no-headers $SPINNER_PPID)
        if [ -z "$SPINNER_PARENTUP" ]; then
          break 2
        fi
      fi
    done
  done
  tput rc
  restore_cursor
  return 0
}

# run_ok - 运行命令或函数、启动微调器并打印确认的函数
# 完成后的指示器。
RUN_LOG="run.log"

# 检查 shell 中是否支持 unicode
# 这是一个奇怪的功能，但似乎有效。
# 检查 unicode 字符是否可以写入文件并可以读回。
shell_has_unicode() {
  # 将 unicode 字符写入文件...读回它并查看是否处理正确。
  env printf "\\u2714" >unitest.txt

  read -r unitest <unitest.txt
  rm -f unitest.txt
  if [ ${#unitest} -le 3 ]; then
    return 0
  else
    return 1
  fi
}

# 使用我们的偏好设置微调器。
SPINNER_COLORCYCLE=0
SPINNER_COLORNUM=6
if shell_has_unicode; then
  SPINNER_SYMBOLS="WIDE_UNI_GREYSCALE2"
else
  SPINNER_SYMBOLS="WIDE_ASCII_PROG"
fi
SPINNER_CLEAR=0 # 不要将该行留空，这样我们的 check/x 就可以简单地覆盖它。

# 执行一个操作，记录它，如果失败则打印彩色复选标记或 X
# 成功则返回0，$? 如果失败。

ComputingColumn() {
  SPINNER_CMD=$(ps -p "$$" -o cmd= | awk -F ' ' '{print $1}')
  case $SPINNER_CMD in
  bash)
    catsh=bash
    ;;
  dash)
    catsh=dash
    ;;
  sh | *)
    # shellcheck disable=SC2012
    catsh=$(ls -al /bin/sh | awk -F '-> ' '{print $2}')
    ;;
  esac

  num="$((num_cn + num_en))"
  case $num in
  1)
    COL=$((columns - ${#msg} - 3))
    ;;
  2)
    case $catsh in
    bash)
      COL=$((columns - ${#msg_cn} * 2 - space - 3))
      ;;
    dash | *)
      msg_cn=$(echo "$msg_cn" | tr -d '[:space:]')
      msg_cn_num=$(echo "$msg_cn" | wc -L)
      msg_cn_quantity=$((msg_cn_num / 2))
      COL=$((columns - ((${#msg_cn} - msg_cn_quantity * 1) + space) - 3))
      ;;
    esac
    ;;
  3)
    msg_cn=$(echo "$msg_cn" | tr -d '[:space:]')
    msg_en=$(echo "$msg_en" | tr -d '[:space:]')
    case $catsh in
    bash)
      COL=$((columns - (${#msg_cn} * 2 + ${#msg_en} + space) - 3))
      ;;
    dash | *)
      msg_cn_num=$(echo "$msg_cn" | wc -L)
      msg_cn_quantity=$((msg_cn_num / 2))
      COL=$((columns - (${#msg_cn} - msg_cn_quantity * 1 + ${#msg_en} + space) - 3))
      ;;
    esac
    ;;
  esac
}

task() {
  charcount='0'
  reply=''
  case $1 in
  enter)
    # shellcheck disable=SC2154
    printf "%s" "${GREEN}""$prompt " "${CYAN}"[1"${YELLOW}"-"${CYAN}""$Index"]"${YELLOW}":"${NORMAL}"
    ;;
  error)
    printf "%s\\n${YELLOW}请输入数字 ${CYAN}[1]${YELLOW} 到 ${CYAN}[$Index]${YELLOW}:${NORMAL} "
    ;;
  secret)
    prompt="${GREEN}${2}:${NORMAL} "
    printf "%s$prompt"
    ;;
  info)
    issue="${GREEN}${2}:${NORMAL} "
    printf "%s$issue"
    ;;
  esac
  while :; do
    char=$(
      stty cbreak -echo
      dd if=/dev/tty bs=1 count=1 2>/dev/null
      stty -cbreak echo
    )
    case $char in
    #检测空字符 NULL
    "$(printenv '\000')")
      break
      ;;
    #检测退格符
    "$(printf '\b')")
      if [ $charcount -gt 0 ]; then
        printf '\b \b'
        reply="${reply%?}"
        charcount=$((charcount - 1))
      else
        printf ''
      fi
      ;;
    "$(printf '\033')")
      mode=$(
        stty cbreak -echo
        dd if=/dev/tty bs=4 count=1 2>/dev/null
        stty -cbreak echo
      )
      case $mode in
      '[A' | '[D')
        printf '\b'
        ;;
      '[B' | '[C')
        printf '\033[C'
        ;;
      '[3~')
        if [ $charcount -gt 0 ]; then
          printf '\033[3~ \033[3~'
          reply="${reply%?}"
          charcount=$((charcount - 1))
        else
          printf ''
        fi
        ;;
      esac
      ;;
    *)
      case $1 in
      secret)
        printf '*'
        ;;
      enter | error | info)
        printf "%s$char"
        ;;
      esac
      reply="${reply}${char}"
      charcount=$((charcount + 1))
      ;;
    esac
  done
  printf '\n' >&2
}

run() {
  # Shell 在传递字符串方面确实很笨拙。
  # 这会传递未展开的 $1 和 $2
  # 因此后续用户可以获得整个内容。
  local cmd="${2}"
  local msg="${3}"
  local log_pref
  log_pref="$(log_date "信息")"
  local columns
  space=$(echo "$msg" | grep -o '[^[:alnum:]]' | grep -c '[^[:alnum:]]')
  # shellcheck disable=SC2001
  msg1=$(echo "$msg" | sed -e 's/[^[:alnum:]]//g')
  msg_cn=$(echo "$msg1" | grep -Po '[\p{Han}]+')
  msg_en=$(echo "$msg1" | grep -Po '\w+')
  num_cn=$(echo "${msg_cn}" | sed -e "{ s/^\(.\).*/\1/ ; q }" | wc -L)
  num_en=$(echo "${msg_en}" | sed -e "{ s/^\(.\).*/\1/ ; q }" | wc -L)

  if [ "${INTERACTIVE_MODE}" != "off" ]; then
    columns=$(tput cols)
    if [ "$columns" -ge 80 ]; then
      columns=79
    fi
  else
    columns=79
  fi

  case $1 in
  ok)
    ComputingColumn
    printf "%s%${COL}s" "${GREEN}$3${NORMAL}"
    ;;
  set)
    ComputingColumn
    number=$(($(grep -o 'run set' "$(find / -name "$0" 2>/dev/null)" | wc -l) - 1))
    # shellcheck disable=SC3006
    set_i=$((set_i + 1)) || true
    if [ $set_i -lt 10 ]; then
      if [ $number -lt 10 ]; then
        COL=$((COL - 6))
      elif [ $number -ge 10 ] && [ $number -lt 100 ]; then
        COL=$((COL - 7))
      elif [ $number -ge 100 ] && [ $number -lt 1000 ]; then
        COL=$((COL - 8))
      fi
    elif [ $set_i -lt 100 ]; then
      if [ $number -lt 100 ]; then
        COL=$((COL - 8))
      elif [ $number -lt 1000 ]; then
        COL=$((COL - 9))
      fi
    elif [ $set_i -lt 1000 ]; then
      COL=$((COL - 10))
    fi
    setnum="${GREEN}[${NORMAL}${YELLOW}${set_i}${NORMAL}/${GREEN}${number}${NORMAL}${GREEN}] ${NORMAL}"
    setcol=$((columns - ${#setnum} - 3))
    printf "%${setcol}s%${COL}s" "${setnum}${GREEN}$3${NORMAL}"
    ;;
  esac
  # 确保 shell 中有一些 unicode 操作
  # 无法以符合 POSIX 的方式检查终端
  # 但术语大多位于 shell 之前。
  # run_ok 函数的 Unicode 复选标记和 x 标记
  CHECK='\u2714'
  BALLOT_X='\u2718'
  if [ "${INTERACTIVE_MODE}" != "off" ]; then
    stty -echo 1>/dev/null 2>&1 1>/dev/null 2>&1
    spinner &
    spinpid=$!
    allpids="$allpids $spinpid"
    if [ "$1" = ok ]; then
      echo "$log_pref 自旋 pid 为： $spinpid" >>"${RUN_LOG}"
    elif [ "$1" = set ]; then
      echo "$log_pref 自旋 pid 为： $spinpid" >>"${RUN_LOG}"
    fi
  fi
  eval "${cmd}" 1>>"${RUN_LOG}" 2>&1
  local res=$?
  touch ${SPINNER_DONEFILE}
  env sleep .4 # 可能会发生标准输出和旋转器的竞争，从而破坏下一点
  # 以防万一旋转器以某种方式幸存下来，杀死它。
  if [ "${INTERACTIVE_MODE}" != "off" ]; then
    pidcheck=$(ps --no-headers "${spinpid}")
    if [ -n "$pidcheck" ]; then
      if [ "$1" = ok ]; then
        echo "$log_pref 到这里...为什么？" >>"${RUN_LOG}"
      elif [ "$1" = set ]; then
        echo "$log_pref 到这里...为什么？" >>"${RUN_LOG}"
      fi
      kill "$spinpid" 2>/dev/null
      rm -rf ${SPINNER_DONEFILE} 2>/dev/null 2>&1
      tput rc
      restore_cursor
    fi
  fi
  # 记录我们应该运行的内容
  msg_safe=$(echo "$msg" | prepare_log_for_nonterminal)

  if [ "$1" = ok ]; then
    printf "$log_pref ${msg_safe}: " >>"${RUN_LOG}"
  elif [ "$1" = set ]; then
    printf "$log_pref ${msg_safe}: " >>"${RUN_LOG}"
  fi
  if shell_has_unicode; then
    if [ $res -eq 0 ]; then
      printf "$log_pref 成功。\\n" >>"${RUN_LOG}"
      env printf "${GREENBG} ${CHECK} ${NORMAL}\\n"
      return 0
    else
      printf "$log_pref 因错误而失败： ${res}\\n" >>"${RUN_LOG}"
      env printf "${REDBG} ${BALLOT_X} ${NORMAL}\\n"
      if [ "$RUN_ERRORS_FATAL" ]; then
        echo
        log_fatal "出了点问题。退出。"
        log_fatal "最后几条日志条目是："
        tail -17 "${RUN_LOG}" | head -15
        exit 1
      fi
      return ${res}
    fi
  else
    if [ $res -eq 0 ]; then
      printf "$log_pref 成功。\\n" >>"${RUN_LOG}"
      env printf "${GREENBG} OK ${NORMAL}\\n"
      return 0
    else
      printf "$log_pref 因错误而失败： ${res}\\n" >>"${RUN_LOG}"
      env printf "${REDBG} ER ${NORMAL}\\n"
      if [ "$RUN_ERRORS_FATAL" ]; then
        log_fatal "上一个命令出错。退出。"
        exit 1
      fi
      return ${res}
    fi
  fi
}

# 问一个是或否的问题
# 如果 $skipyesno 为 1，则始终为 Y
# 如果 NONINTERACTIVE 环境变量为 1，则始终为 N，并打印错误消息以使用 --force
yesno() {
  # XXX skipyesno 是调用脚本中的全局集
  # shellcheck disable=SC2154
  if [ "$skipyesno" = "1" ]; then
    return 0
  fi
  if [ "$VIRTUALMIN_NONINTERACTIVE" = "1" ]; then
    return 0
  fi
  if [ "$NONINTERACTIVE" = "1" ]; then
    echo "检测到非交互式 shell。 无法继续，因为脚本可能需要提出问题。"
    return 1
  fi
  stty echo 1>/dev/null 2>&1
  while read -r line; do
    stty -echo 1>/dev/null 2>&1
    case $line in
    y | Y | Yes | YES | yes | yES | yEs | YeS | yeS)
      return 0
      ;;
    n | N | No | NO | no | nO)
      return 1
      ;;
    *)
      stty echo 1>/dev/null 2>&1
      printf "\\n${YELLOW}请输入 ${CYAN}[y]${YELLOW} 或 ${CYAN}[n]${YELLOW}:${NORMAL} "
      ;;
    esac
  done
  stty -echo 1>/dev/null 2>&1
}

# mkdir 如果不存在
testmkdir() {
  if [ ! -d "$1" ]; then
    mkdir -p "$1"
  fi
}

# 如果目标不存在则复制文件
testcp() {
  if [ ! -e "$2" ]; then
    cp "$1" "$2"
  fi
}

# 设置 Webmin 指令或添加它（如果不存在）
setconfig() {
  sc_config="$2"
  sc_value="$1"
  sc_directive=$(echo "$sc_value" | cut -d'=' -f1)
  if grep -q "$sc_directive $2"; then
    sed -i -e "s#$sc_directive.*#$sc_value#" "$sc_config"
  else
    echo "$1" >>"$2"
  fi
}

# 检测主 IP 地址
# 适用于大多数 Linux 和 FreeBSD（也许）
detect_ip() {
  defaultdev=$(ip ro ls | grep default | head -1 | sed -e 's/.*\sdev\s//g' | awk '{print $1}')
  primaryaddr=$(ip -f inet addr show dev "$defaultdev" | grep 'inet ' | awk '{print $2}' | head -1 | cut -d"/" -f1 | cut -f1)
  if [ "$primaryaddr" ]; then
    log_debug "主地址检测为 $primaryaddr"
    address=$primaryaddr
    return 0
  else
    log_warning "无法确定主接口的 IP 地址。"
    echo "请输入您的主要网络接口的名称： "
    stty echo 1>/dev/null 2>&1
    read -r primaryinterface
    stty -echo 1>/dev/null 2>&1
    #primaryaddr=`/sbin/ifconfig $primaryinterface|grep 'inet addr'|cut -d: -f2|cut -d" " -f1`
    primaryaddr=$(/sbin/ip -f inet -o -d addr show dev "$primaryinterface" | head -1 | awk '{print $4}' | head -1 | cut -d"/" -f1)
    if [ "$primaryaddr" = "" ]; then
      # 使用 FreeBSD 格式重试
      primaryaddr=$(/sbin/ifconfig "$primaryinterface" | grep 'inet' | awk '{ print $2 }')
    fi
    if [ "$primaryaddr" ]; then
      log_debug "主地址检测为 $primaryaddr"
      address=$primaryaddr
    else
      fatal "无法确定所选接口的 IP 地址。 无法继续。"
    fi
    return 0
  fi
}

# 在cloud-init中设置主机名
set_hostname_cloud() {
  # 如果安装了 cloud-init，请保留 Virtualmin 设置的主机名
  if [ -f "/etc/cloud/cloud.cfg" ]; then
    if grep "^preserve_hostname: false" /etc/cloud/cloud.cfg >/dev/null; then
      log_debug "在 /etc/cloud/cloud.cfg 中将 preserve_hostname 设置为 true"
      sed -i "s/^preserve_hostname: false/preserve_hostname: true/" /etc/cloud/cloud.cfg
    fi
  fi
}

# 设置主机名
set_hostname() {
  local i=0
  local forcehostname
  if [ -n "$1" ]; then
    forcehostname=$1
  fi
  while [ $i -le 3 ]; do
    if [ -z "$forcehostname" ]; then
      local name
      name=$(hostname -f)
      log_error "您的系统主机名 $name 不完全合格。"
      printf "请输入完整的主机名 (例如: host.example.com): "
      stty echo 1>/dev/null 2>&1
      read -r line
      stty -echo 1>/dev/null 2>&1
    else
      log_debug "将主机名设置为 $forcehostname"
      line=$forcehostname
    fi
    if ! is_fully_qualified "$line"; then
      i=$((i + 1))
      log_warning "主机名 $line 不完全合格。"
      if [ "$i" = "4" ]; then
        fatal "无法设置完全合格的主机名。"
      fi
    else
      hostname "$line"
      echo "$line" >/etc/hostname
      hostnamectl set-hostname "$line" 1>/dev/null 2>&1
      set_hostname_cloud
      detect_ip
      shortname=$(echo "$line" | cut -d"." -f1)
      if grep "^$address" /etc/hosts >/dev/null; then
        log_debug "IP $address 条目存在于 /etc/hosts 中。"
        log_debug "使用新主机名更新。"
        sed -i "s/^$address.*/$address $line $shortname/" /etc/hosts
      else
        log_debug "将 $address 上的主机名 $line 的新条目添加到 /etc/hosts。"
        printf "%s\\t%s\\t%s\\n" "$address" "$line" "$shortname" >>/etc/hosts
      fi
      i=4
    fi
  done
}

is_fully_qualified() {
  case $1 in
  localhost.localdomain)
    log_warning "主机名不能是 localhost.localdomain。"
    return 1
    ;;
  *.localdomain)
    log_warning "主机名不能是 *.localdomain。"
    return 1
    ;;
  *.internal)
    log_warning "主机名不能是 *.internal。"
    return 1
    ;;
  *.*)
    log_debug "主机名完全符合 $1 条件"
    return 0
    ;;
  esac
  return 1
}

# 设置发行版本全局变量 os_type、os_version、os_major_version、os_real
# 如果失败则返回 1。
get_distro() {
  os=$(uname -o)
  # Make sure we're Linux
  if echo "$os" | grep -iq linux; then
    if [ -f /etc/cloudlinux-release ]; then # Oracle
      local os_string
      os_string=$(cat /etc/cloudlinux-release)
      os_real='CloudLinux'
      os_pretty=$os_string
      os_type='cloudlinux'
      os_version=$(echo "$os_string" | grep -o '[0-9\.]*')
      os_major_version=$(echo "$os_version" | cut -d '.' -f1)
    elif [ -f /etc/oracle-release ]; then # Oracle
      local os_string
      os_string=$(cat /etc/oracle-release)
      os_real='Oracle Linux'
      os_pretty=$os_string
      os_type='ol'
      os_version=$(echo "$os_string" | grep -o '[0-9\.]*')
      os_major_version=$(echo "$os_version" | cut -d '.' -f1)
    elif [ -f /etc/redhat-release ]; then # RHEL/CentOS/Alma/Rocky
      local os_string
      os_string=$(cat /etc/redhat-release)
      isrhel=$(echo "$os_string" | grep 'Red Hat')
      iscentosstream=$(echo "$os_string" | grep 'CentOS Stream')
      if [ -n "$isrhel" ]; then
        os_real='RHEL'
      elif [ -n "$iscentosstream" ]; then
        os_real='CentOS Stream'
      else
        os_real=$(echo "$os_string" | cut -d' ' -f1) # Doesn't work for Scientific
      fi
      os_pretty=$os_string
      os_type=$(echo "$os_real" | tr '[:upper:]' '[:lower:]' | tr ' ' '_')
      os_version=$(echo "$os_string" | grep -o '[0-9\.]*')
      os_major_version=$(echo "$os_version" | cut -d '.' -f1)
    elif [ -f /etc/os-release ]; then # Debian/Ubuntu
      # 获取它，这样我们就可以检查 VERSION_ID
      # shellcheck disable=SC1091
      . /etc/os-release
      # 从技术上来说不正确，但是 os-release 没有适用于 centos 的 7.xxx
      # shellcheck disable=SC2153
      os_real=$NAME
      os_pretty=$PRETTY_NAME
      os_type=$ID
      os_version=$VERSION_ID
      os_major_version=$(echo "${os_version}" | cut -d'.' -f1)
    else
      printf "${RED}未找到 /etc/*-release 文件，可能不支持此操作系统。${NORMAL}\\n"
      return 1
    fi
  else
    printf "${RED}无法检测到支持的操作系统。${NORMAL}\\n"
    return 1
  fi
  if [ -n "$1" ]; then
    case $1 in
    real)
      echo "$os_real"
      ;;
    type)
      echo "$os_type"
      ;;
    version)
      echo "$os_version"
      ;;
    major)
      echo "$os_major_version"
      ;;
    *)
      printf "${RED}未知的参数${NORMAL}\\n"
      return 1
      ;;
    esac
  fi
  return 0
}

# memory_ok - 检查是否有足够内存的函数。 如果没有
# 将通过添加交换文件来修复它。
memory_ok() {
  min_mem=$1
  disk_space_required=$2
  # 如果尚未设置 Virtualmin swap，请尝试设置
  is_swap=$(swapon -s | grep /swap.vm)
  if [ -n "$is_swap" ]; then
    if [ -z "$min_mem" ]; then
      min_mem=1048576
    fi
    # 检查可用 RAM 和交换区
    mem_total=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
    swap_total=$(awk '/SwapTotal/ {print $2}' /proc/meminfo)
    all_mem=$((mem_total + swap_total))
    swap_min=$((1286144 - all_mem))

    if [ "$swap_min" -lt '262144' ]; then
      swap_min=262144
    fi

    min_mem_h=$((min_mem / 1024))
    if [ "$all_mem" -gt "$min_mem" ]; then
      log_debug "内存大于 ${min_mem_h} MB，应该足够了。"
      return 0
    else
      log_error "内存低于 ${min_mem_h} MB。 可能无法进行完整安装。"
    fi

    # 我们需要交换，所以询问并打开一些。
    swap_min_h=$((swap_min / 1024))
    echo
    echo "  您的系统的可用内存和交换空间少于 ${min_mem_h} MB。"
    echo "  安装可能会失败，特别是在 Debian/Ubuntu 系统上（安"
    echo "  装大量软件包时 apt-get 会变得非常大）。 你可以退出"
    echo "  您可以退出并使用 --minimal 标志重新安装，以安装更紧凑的软件包选择"
    echo "  或者我们可以尝试为您创建一个交换文件。要创建交换文件，除了用于安装软件包的"
    echo "  $disk_space_required GB 可用空间之外"
    echo "  您还需要 ${swap_min_h} MB 可用磁盘空间。"
    echo
    echo "  你想继续吗？ 如果继续"
    printf "  您将可以选择创建交换文件。 (y/n) "
    if ! yesno; then
      return 1 # 当该函数返回 1 时应退出
    fi
    echo
    echo "  您想让我尝试创建一个交换文件吗？"
    echo "   除了用于安装的 $disk_space_required GB 之外，这还需要至少 ${swap_min_h} MB 的"

    printf "  可用空间。 (y/n) "
    if ! yesno; then
      log_warning "继续而不创建交换文件。 安装可能会失败。"
      return 0
    fi

    # 检查 btrfs，因为它无法安全地托管交换文件。
    root_fs_type=$(grep -v "^$\\|^\\s*#" /etc/fstab | awk '{print $2 " " $3}' | grep "/ " | cut -d' ' -f2)
    if [ "$root_fs_type" = "btrfs" ]; then
      log_fatal "您的根文件系统似乎正在运行 btrfs。"
      log_fatal "在 btrfs 文件系统上创建交换文件是不安全的。"
      log_fatal "您需要使用 --minimal 安装或手动创建交换文件（在某些其他文件系统上）。"
      return 2
    fi

    # 检查是否有足够的空间。
    root_fs_avail=$(df / | grep -v Filesystem | awk '{print $4}')
    if [ "$root_fs_avail" -lt $((swap_min + 358400)) ]; then
      root_fs_avail_h=$((root_fs_avail / 1024))
      log_fatal "根文件系统只有 $root_fs_avail_h MB 可用，这太小了。"
      log_fatal "您需要使用 --minimal 安装向 '/' 添加更多空间。"
      return 3
    fi

    # Create a new file
    if ! dd if=/dev/zero of=/swap.vm bs=1024 count=$swap_min 1>>${RUN_LOG} 2>&1; then
      log_fatal "创建交换文件 /swap.vm 失败。"
      return 4
    fi
    chmod 0600 /swap.vm 1>>${RUN_LOG} 2>&1
    mkswap /swap.vm 1>>${RUN_LOG} 2>&1
    if ! swapon /swap.vm 1>>${RUN_LOG} 2>&1; then
      log_fatal "启用交换文件失败。 如果这是虚拟机，您的提供商可能会禁止它。"
      return 5
    fi
    echo "/swap.vm          swap            swap    defaults        0 0" >>/etc/fstab
  fi
  return 0
}

password() {
  printf "${underline}\\n"
  printf "%s\\n" "设置密码"
  printf "%s${CYAN}注意:密码不得少于8位、必须包含数字、大小写字母、特殊符号${NORMAL}\\n"
  password=
  while [ -z "$password" ]; do
    task secret 请输入密码
    pwd1=$reply
    if [ -z "$pwd1" ]; then
      printf "%s${RED}密码不能为空，请重新输入${NORMAL}\\n"
    else
      num=${#pwd1}
      digit=$(echo "$pwd1" | grep -o '[[:digit:]]')
      lower=$(echo "$pwd1" | grep -o '[[:lower:]]')
      upper=$(echo "$pwd1" | grep -o '[[:upper:]]')
      alnum=$(echo "$pwd1" | grep -o '[^[:alnum:]]')

      if [ 7 -ge "$num" ] ||
        #必须包含数字
        [ -z "$digit" ] ||
        #必须包含小写字母
        [ -z "$lower" ] ||
        #必须包含大写字母
        [ -z "$upper" ] ||
        #必须包含特殊符号
        [ -z "$alnum" ]; then

        printf "%s${RED}密码不得少于8位、必须包含数字、大小写字母、特殊符号，请重新输入${NORMAL}\\n"
      else
        task secret 请确认密码
        pwd2=$reply
        if [ -z "$pwd2" ]; then
          printf "%s${RED}密码不能为空，请重新输入${NORMAL}\\n"
        else
          if [ "$pwd1" = "$pwd2" ]; then
            password=$pwd2
            echo
            printf "密码设置 ${GREEN}成功${NORMAL} !\\n"
            printf "${underline}\\n"
            break
          else
            printf "%s${RED}您两次输入的密码不一致，请重新输入${NORMAL}\\n"
          fi
        fi
      fi
    fi
  done
}

fircmd() {
  for fircmd_port in "$@"; do
    case $fircmd_port in
    53)
      log_debug "开始配置 tcp:$fircmd_port 端口"
      firewall-cmd --zone=public --add-port="$fircmd_port"/tcp --permanent
      log_debug "配置 tcp:$fircmd_port 端口完成"
      log_debug "开始配置 udp:$fircmd_port 端口"
      firewall-cmd --zone=public --add-port="$fircmd_port"/udp --permanent
      log_debug "配置 udp:$fircmd_port 端口完成"
      ;;
    *)
      log_debug "开始配置 tcp:$fircmd_port 端口"
      firewall-cmd --zone=public --add-port="$fircmd_port"/tcp --permanent
      log_debug "配置 tcp:$fircmd_port 端口完成"
      ;;
    esac
  done
  firewall-cmd --reload
}

ufwcmd() {
  ufwstatus=$(sudo ufw status | awk 'NR==1' | awk -F ': ' '{print $2}')
  if [ "$ufwstatus" = inactive ]; then
    log_debug "开启 UFW 防火墙"
    echo y | sudo ufw enable >/dev/null 2>&1
  fi
  for ufwcmd_port in "$@"; do
    log_debug "开始配置 $ufwcmd_port 端口"
    sudo ufw allow "$ufwcmd_port"
    log_debug "配置 $ufwcmd_port 端口完成"
  done
}

setfr() {
  if [ -x /usr/sbin/ufw ]; then
    setufw="ufwcmd"
  elif [ -x /usr/bin/firewall-cmd ]; then
    setufw="fircmd"
  fi
  for port in "$@"; do
    $setufw "$port"
  done
}

MainMenu() {
  local prompt="$1"
  local name="$2"
  local Index=0
  shift 2
  for i in "$@"; do
    msg="$i"
    ComputingColumn
    Index=$((Index + 1))
    val=$((Index % 1))
    printf "${NORMAL}\t(%1d) %-${COL}s${NORMAL}" "${Index}" "${msg}"
    if [ ${val} -eq 0 ]; then
      printf "\n"
    fi
  done
  printf "\n"
  task enter
  while :; do
    index=$reply
    if [ -n "$reply" ]; then
      if [ "$reply" -le "$Index" ]; then
        reply=writing
      fi
    fi
    case $reply in
    writing)
      set -- "$@"
      shift "$((index - 1))"
      eval "${name}='$1'"
      break
      ;;
    *)
      task error
      ;;
    esac
  done
}

title() {
  clear
  local TITLECOL
  msg="$1"
  ComputingColumn
  TITLECOLL=$((COL / 2))
  printf "%${TITLECOLL}s""${YELLOW}$msg${NORMAL}\n"
}

disable_selinux() {
  seconfigfiles="/etc/selinux/config /etc/sysconfig/selinux"
  for i in $seconfigfiles; do
    if [ -e "$i" ]; then
      perl -pi -e 's/^SELINUX=.*/SELINUX=disabled/' "$i"
    fi
  done
}

tarzip() {
  case ${1##*.} in
  tar)
    tar="tar -xvf"
    ;;
  gz | tgz)
    tar="tar -xzvf"
    ;;
  bz2)
    tar="tar -xjvf"
    ;;
  Z)
    tar="tar -xZvf"
    ;;
  xz)
    tar="tar -Jxvf"
    ;;
  esac
  # shellcheck disable=SC2154
  run ok "$tar $1 -C $install_path/" "正在解压 $1"
}

install_env() {
  case "$os_type" in
  rhel | centos | fedora | ubuntu | debian | rocky)
    if [ -x /usr/bin/dnf ]; then
      installtype=dnf
      if [ "$(id -u)" -eq 0 ]; then
        install="dnf -y install"
        remove="dnf -y autoremove"
        update="dnf -y update"
        upgrade="dnf -y upgrade"
        updatelist="dnf check-update --quiet --assumeno"
        install_info="dnf info"
      else
        install="sudo dnf -y install"
        remove="sudo dnf -y autoremove"
        update="sudo dnf -y update"
        upgrade="sudo dnf -y upgrade"
        updatelist="sudo dnf check-update --quiet --assumeno"
        install_info="sudo dnf info"
      fi
    elif [ -x /usr/bin/yum ]; then
      installtype=yum
      if [ "$(id -u)" -eq 0 ]; then
        install="yum -y install"
        remove="yum -y autoremove"
        update="yum -y update"
        upgrade="yum -y upgrade"
        updatelist="yum check-update --quiet --assumeno"
        install_info="rpm -qi"
      else
        install="sudo yum -y install"
        remove="sudo yum -y autoremove"
        update="sudo yum -y update"
        upgrade="sudo yum -y upgrade"
        updatelist="sudo yum check-update --quiet --assumeno"
        install_info="sudo rpm -qi"
      fi
    elif [ -x /usr/bin/apt ]; then
      installtype=apt
      if [ "$(id -u)" -eq 0 ]; then
        install="apt -q -y install"
        remove="apt autoremove --assume-yes --purge"
        update="apt -y update"
        upgrade="apt -y upgrade"
        updatelist="apt list --upgradable"
        install_info="apt-cache policy"

      else
        install="sudo apt -q -y install"
        remove="sudo apt autoremove --assume-yes --purge"
        update="sudo apt-get -y update"
        upgrade="sudo apt-get -y upgrade"
        updatelist="sudo apt list --upgradable"
        install_info="sudo apt-cache policy"
      fi
    elif [ -x /usr/bin/apt-get ]; then
      installtype=apt-get
      if [ "$(id -u)" -eq 0 ]; then
        install="apt-get -q -y install"
        remove="apt-get autoremove --assume-yes --purge"
        update="apt-get -y update"
        upgrade="apt-get -y upgrade"
        updatelist="apt-get list --upgradable"
        install_info="apt-cache policy"
      else
        install="sudo apt-get -q -y install"
        remove="sudo apt-get autoremove --assume-yes --purge"
        update="sudo apt-get -y update"
        upgrade="sudo apt-get -y upgrade"
        updatelist="sudo apt-get list --upgradable"
        install_info="sudo apt-cache policy"
      fi
    fi
    ;;
  *)
    fatal "无法识别此操作系统/版本。无法继续。"
    exit 1
    ;;
  esac
}

check_install() {
  DEBIAN_FRONTEND="noninteractive"
  for package in "$@"; do
    case $installtype in
    dnf)
      # shellcheck disable=SC2154
      if [ "$lang_cut" = "zh_CN" ]; then
        Installed=$($install_info "${package}" | grep -B1 名称 | grep -v 名称 | awk 'NR==1 {print}')
      else
        Installed=$($install_info "${package}" | grep -B1 Name | grep -v Name | awk 'NR==1 {print}')
      fi
      if [ "$Installed" = "Available Packages" ] || [ "$Installed" = "可安装的软件包" ]; then
        run ok "$install $package" "安装 $package"
      else
        log_debug "$package 已安装"
        # shellcheck disable=SC2154
        if $updatelist "$package" 2>/dev/null | grep -q "$pkg_name"; then
          run ok "$upgrade $package" "升级 $package"
          log_debug "$package 已升级"
        fi
      fi
      ;;
    yum)
      if [ "$lang_cut" = "zh_CN" ]; then
        Installed=$($install_info "${package}" | awk -F ' ' '{print $1}')
      else
        Installed=$($install_info "${package}" | awk -F ' ' '{print $1}')
      fi
      if [ "$Installed" = "package" ] || [ "$Installed" = "未安装软件包" ]; then
        run ok "$install $package" "安装 $package"
      else
        log_debug "$package 已安装"
        if $updatelist "$package" 2>/dev/null | grep -q "$pkg_name"; then
          run ok "$upgrade $package" "升级 $package"
          log_debug "$package 已升级"
        fi
      fi
      ;;
    apt | apt-get)
      if [ "$lang_cut" = "zh_CN" ]; then
        Installed=$($install_info "${package}" | awk 'NR==2 {print}' | awk -F "：" '{print $2}')
      else
        Installed=$($install_info "${package}" | awk 'NR==2 {print}' | awk -F ": " '{print $2}')
      fi

      if [ "$Installed" = "(none)" ] || [ "$Installed" = "(无)" ]; then
        run ok "$install $package" "安装 $package"
      else
        log_debug "$package 已安装"
        if $updatelist "$package" 2>/dev/null | grep -q "$pkg_name"; then
          run ok "$upgrade $package" "升级 $package"
          log_debug "$package 已升级"
        fi
      fi
      ;;
    esac
  done
}
